#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim: set ts=4 sw=4 noet:
#
# $Id: FindDialog.py 2 2008-06-05 13:15:18Z wejradford $

"""
FindDialog.py
"""

__version__ = "$Revision: 2 $"
# $URL: http://yare-gedit-plugin.googlecode.com/svn/trunk/yare/FindDialog.py $

import gtk, re, os, logging
from gettext import gettext as _
from RegexEngine import RegexEngine, InvalidPatternError, NoPatternError, RegexError
from HistoryProvider import HistoryProvider
from PreviewManager import PreviewManager
from HighlightManager import HighlightManager

HISTORY_PATH = os.path.dirname(os.path.dirname(__file__))
FIND_HISTORY = os.path.join(HISTORY_PATH, "find_history.txt")
REPLACE_HISTORY = os.path.join(HISTORY_PATH, "replace_history.txt")

ui_str = """<ui>
  <menubar name="MenuBar">
	<menu name="SearchMenu" action="Search">
      <placeholder name="SearchOps_2">
        <menuitem name="Regex Find and Replace" action="LaunchFindDialog"/>
      </placeholder>
    </menu>
  </menubar>
</ui>
"""

class FindDialog(object):
	ESCAPE_KEYVAL = 65307
	ENTER_KEYVAL = 65293
	def __init__(self, plugin, window, gladeObject):
		self._window = window
		self._plugin = plugin
		self._engine = RegexEngine()
		# Insert menu items
		self._insert_menu()
		self._loadGlade(gladeObject)
		self._previewManager = PreviewManager(self._previewTextView)
		self._highlights = HighlightManager(self._engine)

	def stop(self):
		# Remove any installed menu items
		self._remove_menu()
		self._window = None
		self._plugin = None
		self._action_group = None
		
	def _insert_menu(self):
		# Get the GtkUIManager
		manager = self._window.get_ui_manager()
		# Create a new action group
		self._action_group = gtk.ActionGroup("FindDialogActions")
		tipText = "Finds and Replaces using Regular Expressions."
		self._action_group.add_actions([("LaunchFindDialog", None, _("Regex Find & Replace"), "<Ctrl>r", tipText, self.launchFindDialog)])
		# Insert the action group
		manager.insert_action_group(self._action_group, -1)
		# Merge the UI
		self._ui_id = manager.add_ui_from_string(ui_str)

	def _loadGlade(self, gladeObject):
		"""
		Loads widgets from the glade object.
		"""
		## Find Dialog.
		self._findDialog = gladeObject.get_widget("FindDialog")
		self._findDialog.connect("key-release-event", self._findDialogKeyReleaseEvent)
		self._findDialog.connect("focus-out-event", self._focusOut)
		self._wrapWindow(self._findDialog)
		self._statusbar = gladeObject.get_widget("statusbar")
		# ComboEntries for search terms.
		findCombo = gladeObject.get_widget("findCombo")
		findCombo.set_model(gtk.ListStore(str))
		findCombo.set_text_column(0)
		findCombo.connect("changed", self._findComboChanged)
		findCombo.connect("key-release-event", self._findComboKeyReleaseEvent)
		self._findCombo = HistoryProvider(findCombo, FIND_HISTORY)
		replaceCombo = gladeObject.get_widget("replaceCombo")
		replaceCombo.set_model(gtk.ListStore(str))
		replaceCombo.set_text_column(0)
		replaceCombo.connect("key-release-event", self._findComboKeyReleaseEvent)
		self._replaceCombo = HistoryProvider(replaceCombo, REPLACE_HISTORY)
		# Expression metadata checkbuttons.
		self._ignorecaseCheckbutton = gladeObject.get_widget("ignorecaseCheckbutton")
		self._singleLineCheckbutton = gladeObject.get_widget("singleLineCheckbutton")
		self._singleLineCheckbutton.connect("toggled", self._singleLineCheckbuttonToggled)
		self._localeCheckbutton = gladeObject.get_widget("localeCheckbutton")
		self._localeCheckbutton.set_active(True)
		self._dotallCheckbutton = gladeObject.get_widget("dotallCheckbutton")
		self._expressionMetadata = {re.I: self._ignorecaseCheckbutton, re.M: self._singleLineCheckbutton,
									re.L: self._localeCheckbutton, re.S: self._dotallCheckbutton}
		# Search setting checkbuttons.
		self._wrapCheckbutton = gladeObject.get_widget("wrapCheckbutton")
		self._globalCheckbutton = gladeObject.get_widget("globalCheckbutton")
		self._highlightCheckbutton = gladeObject.get_widget("highlightCheckbutton")
		self._highlightCheckbutton.connect("toggled", self._highlightCheckbuttonToggled)
		self._previewTextView = gladeObject.get_widget("previewTextView")
		# Search control buttons.
		self._findButton = gladeObject.get_widget("findButton")
		self._findButton.connect("clicked", self._findButtonClicked)
		self._replaceButton = gladeObject.get_widget("replaceButton")
		self._replaceButton.connect("clicked", self._replaceButtonClicked)
		self._allButton = gladeObject.get_widget("allButton")
		self._allButton.connect("clicked", self._allButtonClicked)
		self._helpButton = gladeObject.get_widget("helpButton")
		# Integration with Pattern Manager.
		self._loadButton = gladeObject.get_widget("loadButton")
		self._saveButton = gladeObject.get_widget("saveButton")
		# Hide the dialogs for now.
		self._findDialog.hide()
		# Setup some searching gear.
		self._startMark = None
		self._messageDialog = None

	def _remove_menu(self):
		# Get the GtkUIManager
		manager = self._window.get_ui_manager()
		# Remove the ui
		manager.remove_ui(self._ui_id)
		# Remove the action group
		manager.remove_action_group(self._action_group)
		# Make sure the manager updates
		manager.ensure_update()

	def update(self):
		# Called whenever the window has been updated (active tab
		# changed, etc.)
		pass

	def _wrapWindow(self, window):
		window.connect("delete-event", self._hideWindow)
		window.connect("destroy-event", self._hideWindow)
		window.set_transient_for(self._window)

	def _hideWindow(self, window, event):
		"""
		Removes highlights and hides the window.
		"""
		logging.info("Hiding window - removing highlights...")
		self._highlights.remove(self._getDocumentCursorSelection()[0])
		window.hide()
		return True

	def launchFindDialog(self, action):
		"""
		Launches the find dialog and if there is a current selection, sets it as the pattern.
		"""
		self._presentWindow(self._findDialog)
		document, cursorIter, selectionIter = self._getDocumentCursorSelection()
		# FIXME - Do we want hidden chars???
		if cursorIter != selectionIter:
			text = document.get_text(cursorIter, selectionIter).decode(document.get_encoding().get_charset())
			self._findCombo.text = text
		self._pattern = None
		self._replaceCombo.text = ""
		self._wrapped = False
		self._lastCursorIter = None
		self._lastSelectionIter = None
		self._previewTextView.get_buffer().set_text("")
		self._statusbar.push(0, "")
		self._engine.flush()
		self._findCombo.grab_focus()

	def _presentWindow(self, window):
		# Show.
		window.show_all()
		if not window.is_focus():
			window.present()
		window.grab_focus()

	def _focusOut(self, window, event):
		"""
		Check to see if the buffer has been modified, in which case, re-apply highlights.
		"""
		logging.info("Losing focus: %s - window visible: %s" % (event, self._findDialog.get_property("visible")))
		if self._findDialog.get_property("visible"):
			document = self._getDocumentCursorSelection()[0]
			if document.get_modified():
				nHighlights = self._highlights.reapply(document)
				if nHighlights is not None:
					self._statusbar.push(0, "%d matches found." % nHighlights)

	def _highlightCheckbuttonToggled(self, button):
		"""
		When active, all highlights in the text should be displayed.
		"""
		document = self._getDocumentCursorSelection()[0]
		if button.get_active():
			nHighlights = self._highlights.apply(document)
			self._statusbar.push(0, "%d matches found." % nHighlights)
		else:
			self._highlights.remove(document)

	def _singleLineCheckbuttonToggled(self, button):
		"""
		When active, the preview window should be sensitive
		and contain the previewed replacement.
		Otherwise, it is insensitive and empty.
		"""
		if not button.get_active():
			self._previewManager.clear()
			self._previewTextView.set_property("sensitive", False)
		else:
			self._previewTextView.set_property("sensitive", True)
			document, cursorIter, selectionIter = self._getDocumentCursorSelection()
			encoding = document.get_encoding().get_charset()
			replacement = self._engine.replacement.decode(encoding)
			self._previewManager.populate(document, cursorIter, selectionIter, replacement)

	def _findComboChanged(self, entry):
		"""
		Cleans up the expression and highlighting.
		"""
		if self._findCombo.text != self._pattern:
			self._pattern = None
			self._engine.flush()

	def _findDialogKeyReleaseEvent(self, entry, event):
		if event.keyval == self.ESCAPE_KEYVAL:
			self._hideWindow(self._findDialog, None)

	def _findComboKeyReleaseEvent(self, entry, event):
		if event.keyval == self.ENTER_KEYVAL:
			self._findButtonClicked(None)

	def _setupSearch(self, document):
		"""
		Checks to see if the pattern is the same as the last time.
		Compiles the pattern.
		Stores the current position in the buffer.
		"""
		# Assume that the pattern is encoded the same way as the document.
		self._pattern = self._findCombo.text.decode(document.get_encoding().get_charset())
		didSetup = self._engine.setup(self._pattern, self._buildMetadataDict(self._expressionMetadata))
		if not didSetup:
			return
		self._findCombo.update()
		# Set the mark.
		self._mark = document.create_mark("mark", document.get_iter_at_mark(document.get_insert()))
		if self._highlightCheckbutton.get_active():
			nHighlights = self._highlights.reapply(document)
			self._statusbar.push(0, "%d matches found." % nHighlights)

	def _findButtonClicked(self, button):
		"""
		Calls the internal search function.
		"""
		try:
			result = self._search()
		except RegexError, e:
			self._dialog(str(e))
		else:
			self._findButton.grab_focus()
			if not result:
				self._dialog("No more matches!")

	def _search(self):
		"""
		Sets up the search.
		Moves to the next bit of matching text.
		Returns True if there was a match, False otherwise.
		"""
		document, cursorIter, selectionIter = self._getDocumentCursorSelection()
		self._setupSearch(document) # May raise a RegexError.
		# Find the next match, moving the cursor to the end of any current selection.
		lastCursorIter = cursorIter.copy()
		lastSelectionIter = selectionIter.copy()
		if cursorIter != selectionIter:
			cursorIter.forward_chars(selectionIter.get_offset() - cursorIter.get_offset())
		offset = cursorIter.get_offset()
		# FIXME - What does this mean???
		'''
		# If we're in singleLine mode, then iterate by line.
		if self._singleLineCheckbutton.get_active():
			endIter = cursorIter.copy()
			endIter.forward_to_line_end()
			text = document.get_text(cursorIter, endIter, False).decode(document.get_encoding().get_charset())
		else:
			text = document.get_text(cursorIter, document.get_end_iter(), False).decode(document.get_encoding().get_charset())
		'''
		encoding = document.get_encoding().get_charset()
		text = document.get_text(cursorIter, document.get_end_iter(), False).decode(encoding)
		replacePattern = self._replaceCombo.text.decode(encoding)
		start, end = self._engine.search(text, replacePattern)
		if start is None and end is None:
			# FIXME - What does this mean???
			'''
			if self._singleLineCheckbutton.get_active() and not endIter.is_end():
				document.place_cursor(endIter)
				return False
			'''
			# If there are no more matches, but we're wrapping...
			if self._wrapCheckbutton.get_active():
				# Only store the last cursor position when we wrap.
				self._lastCursorIter = lastCursorIter
				self._lastSelectionIter = lastSelectionIter
				document.place_cursor(document.get_start_iter())
				self._wrapped = True
				return self._search()
			else:
				return False
		else:
			# Select the match and put it in view.
			startIter = document.get_iter_at_offset(offset + start)
			endIter = document.get_iter_at_offset(offset + end)
			# Finish searching if we've wrapped and passed the original mark.
			if self._wrapped and startIter.compare(document.get_iter_at_mark(self._mark)) > 0:
				# If we've wrapped and not found any more matches, we want to leave the last selection active.
				if cursorIter.compare(selectionIter) == 0:
					document.select_range(self._lastCursorIter, self._lastSelectionIter)
				return True
			document.select_range(startIter, endIter)
			self._window.get_active_view().scroll_to_cursor()
			# Only preview changes if we're using single-line mode.
			if self._singleLineCheckbutton.get_active():
				encoding = document.get_encoding().get_charset()
				replacement = self._engine.replacement.decode(encoding)
				self._previewManager.populate(document, startIter, endIter, replacement)
			else:
				self._previewManager.clear()
			return True

	def _replaceButtonClicked(self, button):
		"""
		Makes the replacement (if available), then moves to the next match.
		"""
		if self._engine.replacement is not None:
			document, cursorIter, selectionIter = self._getDocumentCursorSelection()
			self._replaceCombo.update()
			document.begin_user_action()
			document.delete(cursorIter, selectionIter)
			encoding = document.get_encoding().get_charset()
			replacement = self._engine.replacement.decode(encoding)
			document.insert_at_cursor(replacement)
			selectionIter = document.get_iter_at_mark(document.get_insert())
			cursorIter = selectionIter.copy()
			cursorIter.backward_chars(len(replacement))
			document.select_range(cursorIter, selectionIter)
			self._replaceButton.grab_focus()
			document.end_user_action()
		try:
			result = self._search()
		except RegexError, e:
			self._dialog(str(e))
		else:
			if not result:
				self._dialog("No more matches!")

	def _allButtonClicked(self, button):
		"""
		Replaces all matches and shows a Dialog with the number of replacements made.
		"""
		document, cursorIter, selection = self._getDocumentCursorSelection()
		self._replaceCombo.update()
		endIter = document.get_end_iter()
		text = document.get_text(cursorIter, endIter, False).decode(document.get_encoding().get_charset())
		result, replaced = self._engine.subn(self._replaceCombo.text, text)
		document.begin_user_action()
		document.delete(cursorIter, endIter)
		document.insert_at_cursor(result)
		document.end_user_action()
		self._dialog("Made %d replacements." % replaced)

	# Utilities.
	def _getDocumentCursorSelection(self):
		"""
		Returns a 3-tuple of:
			(document, cursorIter, selectionIter)
		"""
		document = self._window.get_active_document()
		cursorIter = document.get_iter_at_mark(document.get_insert())
		selectionIter = document.get_iter_at_mark(document.get_selection_bound())
		return (document, cursorIter, selectionIter)
	
	def _buildMetadataDict(self, inputDict):
		"""
		Returns a dict.
		"""
		return dict([(key, value.get_active()) for key, value in inputDict.iteritems()])

	def _dialog(self, message):
		"""
		Builds, connects and runs a MessageDialog.
		"""
		if self._messageDialog is None:
			self._messageDialog = gtk.MessageDialog(type=gtk.MESSAGE_INFO, buttons=gtk.BUTTONS_OK)
			self._messageDialog.connect("response", lambda *args: args[0].hide())
		self._messageDialog.set_markup(message)
		self._messageDialog.run()

# End of $URL: http://yare-gedit-plugin.googlecode.com/svn/trunk/yare/FindDialog.py $
